import React, { Children } from 'react';
import warning from 'warning';

const DRAG_SIDE_RANGE = 0.25;
const DRAG_MIN_GAP = 2;

let onlyTreeNodeWarned = false;

export default function toArray(children) {
    const ret = [];
    React.Children.forEach(children, c => {
        ret.push(c);
    });
    return ret;
}
export function warnOnlyTreeNode() {
    if (onlyTreeNodeWarned) return;

    onlyTreeNodeWarned = true;
    warning(false, 'Tree only accept TreeNode as children.');
}

export function arrDel(list, value) {
    const clone = list.slice();
    const index = clone.indexOf(value);
    if (index >= 0) {
        clone.splice(index, 1);
    }
    return clone;
}

export function arrAdd(list, value) {
    const clone = list.slice();
    if (clone.indexOf(value) === -1) {
        clone.push(value);
    }
    return clone;
}

export function posToArr(pos) {
    return pos.split('-');
}

export function getPosition(level, index) {
    return `${level}-${index}`;
}

export function isTreeNode(node) {
    return node && node.type && node.type.isTreeNode;
}

export function getNodeChildren(children) {
    return toArray(children).filter(isTreeNode);
}

export function isCheckDisabled(node) {
    const { disabled, disableCheckbox } = node.props || {};
    return !!(disabled || disableCheckbox);
}

export function traverseTreeNodes(treeNodes, callback) {
    function processNode(node, index, parent) {
        const children = node ? node.props.children : treeNodes;
        const pos = node ? getPosition(parent.pos, index) : 0;

        // Filter children
        const childList = getNodeChildren(children);

        // Process node if is not root
        if (node) {
            const data = {
                node,
                index,
                pos,
                key: node.key || pos,
                parentPos: parent.node ? parent.pos : null
            };

            callback(data);
        }

        // Process children node
        Children.forEach(childList, (subNode, subIndex) => {
            processNode(subNode, subIndex, { node, pos });
        });
    }

    processNode(null);
}

/**
 * Use `rc-util` `toArray` to get the children list which keeps the key.
 * And return single node if children is only one(This can avoid `key` missing check).
 */
export function mapChildren(children, func) {
    const list = toArray(children).map(func);
    if (list.length === 1) {
        return list[0];
    }
    return list;
}

/**
 * Check position relation.
 * @param parentPos
 * @param childPos
 * @param directly only directly parent can be true
 * @returns {boolean}
 */
export function isParent(parentPos, childPos, directly = false) {
    if (!parentPos || !childPos || parentPos.length > childPos.length) return false;

    const parentPath = posToArr(parentPos);
    const childPath = posToArr(childPos);

    // Directly check
    if (directly && parentPath.length !== childPath.length - 1) return false;

    const len = parentPath.length;
    for (let i = 0; i < len; i += 1) {
        if (parentPath[i] !== childPath[i]) return false;
    }

    return true;
}

export function getDragNodesKeys(treeNodes, node) {
    const { eventKey, pos } = node.props;
    const dragNodesKeys = [];

    traverseTreeNodes(treeNodes, ({ pos: nodePos, key }) => {
        if (isParent(pos, nodePos)) {
            dragNodesKeys.push(key);
        }
    });
    dragNodesKeys.push(eventKey || pos);
    return dragNodesKeys;
}

// Only used when drag, not affect SSR.
export function calcDropPosition(event, treeNode) {
    const { clientY } = event;
    const { top, bottom, height } = treeNode.selectHandle.getBoundingClientRect();
    const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP);

    if (clientY <= top + des) {
        return -1;
    } else if (clientY >= bottom - des) {
        return 1;
    }

    return 0;
}

/**
 * Return selectedKeys according with multiple prop
 * @param selectedKeys
 * @param props
 * @returns [string]
 */
export function calcSelectedKeys(selectedKeys, props) {
    if (!selectedKeys) return undefined;

    const { multiple } = props;
    if (multiple) {
        return selectedKeys.slice();
    }

    if (selectedKeys.length) {
        return [selectedKeys[0]];
    }
    return selectedKeys;
}

/**
 * Since React internal will convert key to string,
 * we need do this to avoid `checkStrictly` use number match
 */
function keyListToString(keyList) {
    if (!keyList) return keyList;
    return keyList.map(key => String(key));
}

export function convertDataToTree(treeData) {
    if (!treeData) return [];
    const list = Array.isArray(treeData) ? treeData : [treeData];
    return list.map(({ children, ...props }) => {
        const childrenNodes = (children || []).map(convertDataToTree);

        return <TreeNode {...props}>{childrenNodes}</TreeNode>;
    });
}

// TODO: ========================= NEW LOGIC =========================
/**
 * Calculate treeNodes entities.
 * @param treeNodes
 * @param processTreeEntity  User can customize the entity
 */
export function convertTreeToEntities(
    treeNodes,
    { initWrapper, processEntity, onProcessFinished } = {}
) {
    const posEntities = {};
    const keyEntities = {};
    let wrapper = {
        posEntities,
        keyEntities
    };

    if (initWrapper) {
        wrapper = initWrapper(wrapper) || wrapper;
    }

    traverseTreeNodes(treeNodes, item => {
        const { node, index, pos, key, parentPos } = item;
        const entity = { node, index, key, pos };

        posEntities[pos] = entity;
        keyEntities[key] = entity;

        // Fill children
        entity.parent = posEntities[parentPos];
        if (entity.parent) {
            entity.parent.children = entity.parent.children || [];
            entity.parent.children.push(entity);
        }

        if (processEntity) {
            processEntity(entity, wrapper);
        }
    });

    if (onProcessFinished) {
        onProcessFinished(wrapper);
    }

    return wrapper;
}

/**
 * Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
 */
export function parseCheckedKeys(keys) {
    if (!keys) {
        return null;
    }

    // Convert keys to object format
    let keyProps;
    if (Array.isArray(keys)) {
        // [Legacy] Follow the api doc
        keyProps = {
            checkedKeys: keys,
            halfCheckedKeys: undefined
        };
    } else if (typeof keys === 'object') {
        keyProps = {
            checkedKeys: keys.checked || undefined,
            halfCheckedKeys: keys.halfChecked || undefined
        };
    } else {
        warning(false, '`CheckedKeys` is not an array or an object');
        return null;
    }

    keyProps.checkedKeys = keyListToString(keyProps.checkedKeys);
    keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys);

    return keyProps;
}

/**
 * Conduct check state by the keyList. It will conduct up & from the provided key.
 * If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
 * @param keyList       list of keys
 * @param isCheck       is check the node or not
 * @param keyEntities   parsed by `convertTreeToEntities` function in Tree
 * @param checkStatus   Can pass current checked status for process (usually for uncheck operation)
 * @returns {{checkedKeys: [], halfCheckedKeys: []}}
 */
export function conductCheck(keyList, isCheck, keyEntities, checkStatus = {}) {
    const checkedKeys = {};
    const halfCheckedKeys = {}; // Record the key has some child checked (include child half checked)

    (checkStatus.checkedKeys || []).forEach(key => {
        checkedKeys[key] = true;
    });

    (checkStatus.halfCheckedKeys || []).forEach(key => {
        halfCheckedKeys[key] = true;
    });

    // Conduct up
    function conductUp(key) {
        if (checkedKeys[key] === isCheck) return;

        const entity = keyEntities[key];
        if (!entity) return;

        const { children, parent, node } = entity;

        if (isCheckDisabled(node)) return;

        // Check child node checked status
        let everyChildChecked = true;
        let someChildChecked = false; // Child checked or half checked

        (children || [])
            .filter(child => !isCheckDisabled(child.node))
            .forEach(({ key: childKey }) => {
                const childChecked = checkedKeys[childKey];
                const childHalfChecked = halfCheckedKeys[childKey];

                if (childChecked || childHalfChecked) someChildChecked = true;
                if (!childChecked) everyChildChecked = false;
            });

        // Update checked status
        if (isCheck) {
            checkedKeys[key] = everyChildChecked;
        } else {
            checkedKeys[key] = false;
        }
        halfCheckedKeys[key] = someChildChecked;

        if (parent) {
            conductUp(parent.key);
        }
    }

    // Conduct down
    function conductDown(key) {
        if (checkedKeys[key] === isCheck) return;

        const entity = keyEntities[key];
        if (!entity) return;

        const { children, node } = entity;

        if (isCheckDisabled(node)) return;

        checkedKeys[key] = isCheck;

        (children || []).forEach(child => {
            conductDown(child.key);
        });
    }

    function conduct(key) {
        const entity = keyEntities[key];

        if (!entity) {
            warning(false, `'${key}' does not exist in the tree.`);
            return;
        }

        const { children, parent, node } = entity;
        checkedKeys[key] = isCheck;

        if (isCheckDisabled(node)) return;

        // Conduct down
        (children || []).filter(child => !isCheckDisabled(child.node)).forEach(child => {
            conductDown(child.key);
        });

        // Conduct up
        if (parent) {
            conductUp(parent.key);
        }
    }

    (keyList || []).forEach(key => {
        conduct(key);
    });

    const checkedKeyList = [];
    const halfCheckedKeyList = [];

    // Fill checked list
    Object.keys(checkedKeys).forEach(key => {
        if (checkedKeys[key]) {
            checkedKeyList.push(key);
        }
    });

    // Fill half checked list
    Object.keys(halfCheckedKeys).forEach(key => {
        if (!checkedKeys[key] && halfCheckedKeys[key]) {
            halfCheckedKeyList.push(key);
        }
    });

    return {
        checkedKeys: checkedKeyList,
        halfCheckedKeys: halfCheckedKeyList
    };
}

/**
 * If user use `autoExpandParent` we should get the list of parent node
 * @param keyList
 * @param keyEntities
 */
export function conductExpandParent(keyList, keyEntities) {
    const expandedKeys = {};

    function conductUp(key) {
        if (expandedKeys[key]) return;

        const entity = keyEntities[key];
        if (!entity) return;

        const { parent, node } = entity;

        if (isCheckDisabled(node)) return;

        expandedKeys[key] = true;

        if (parent) {
            conductUp(parent.key);
        }
    }

    (keyList || []).forEach(key => {
        conductUp(key);
    });

    return Object.keys(expandedKeys);
}
